package com.orion.config;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.orion.util.ResponseUtil;
import com.orion.domain.Result;
import com.orion.enumeration.CommonCodeEnum;
import com.orion.enumeration.Oauth2ExceptionCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
import java.util.Set;

/**
 * 检验/oauth2/token中的clientId+clientSecret
 *
 * @author Administrator
 * @date 2021/9/28
 */
@Slf4j
@Component
public class CustomBasicTokenFilter extends OncePerRequestFilter {


    /**
     * accessToken 和refreshToken 都使用该链接,只有三种模式走这个链接，隐藏式implicit只走authorize，其responseType=token
     */
    private static final String OAUTH2_AUTHORIZATION_URL = "/oauth/token";
    private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";

    private static final String AUTHORIZATION_CODE = "authorization_code";
    private static final String PASSWORD = "password";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String CLIENT_CREDENTIALS = "client_credentials";

    private static final Set<String> grantTypeSet = CollectionUtil.newHashSet(
            AUTHORIZATION_CODE,
            PASSWORD,
            REFRESH_TOKEN,
            CLIENT_CREDENTIALS);

    @Resource
    private ClientDetailsService clientDetailsService;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        if (!request.getRequestURI().equals(OAUTH2_AUTHORIZATION_URL)) {
            filterChain.doFilter(request, response);
            return;
        }

        String grantType = request.getParameter("grant_type");
        if (StrUtil.isBlank(grantType)) {
            log.info("missing grant type");
            Result r = CommonCodeEnum.COMMON_MISSING_PARAM.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        if (!grantTypeSet.contains(grantType)) {
            log.info("invalid grant type");
            Result r = Oauth2ExceptionCodeEnum.INVALID_GRANT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        //授权码模式需要
        /*
           http://ncsf6sb4:37bc1v7l8c@127.0.0.1:8547/oauth/token?
           scope=user_info&redirect_uri=http://127.0.0.1:8549/client/receiveCode&grant_type=authorization_code&code=Vky6Ie
          //请求头放clientId和clientSecret
         */
        String scope = request.getParameter("scope");
        String redirectUri = request.getParameter("redirect_uri");
        String code = request.getParameter("code");

        //密码模式需要 password
        // http://localhost:8080/oauth/token?username=hellxz&password=xyz&scope=read_scope&grant_type=password
        //请求头放clientId和clientSecret
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        //客户端(凭证)模式只需要grantType
        // http://localhost:8080/oauth/token?grant_type=client_credentials
        //请求头放clientId和clientSecret
        String refreshToken = request.getParameter("refresh_token");

        //刷新token模式只需要grantType和refresh_token
        // http://ncsf6sb4:37bc1v7l8c@127.0.0.1:8547/oauth/token?grant_type=refresh_token&refresh_token=a9d43c25-c181-45a2-ba11-3f8cd5d6f3f4
        //请求头放clientId和clientSecret
        if (grantType.equals(AUTHORIZATION_CODE)) {
            if (StrUtil.isBlank(scope) || StrUtil.isBlank(redirectUri) || StrUtil.isBlank(code)) {
                log.info("{} mode : missing request token param", AUTHORIZATION_CODE);
                Result r = CommonCodeEnum.COMMON_MISSING_PARAM.toResult();
                ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
                return;
            }
        }

        if (grantType.equals(PASSWORD)) {
            if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
                log.info("{} mode : missing request token param", PASSWORD);
                Result r = CommonCodeEnum.COMMON_MISSING_PARAM.toResult();
                ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
                return;
            }
        }

        if (grantType.equals(REFRESH_TOKEN)) {
            if (StrUtil.isBlank(refreshToken)) {
                log.info("{} mode : missing request token param", REFRESH_TOKEN);
                Result r = CommonCodeEnum.COMMON_MISSING_PARAM.toResult();
                ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
                return;
            }
        }

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication != null && authentication.isAuthenticated()) {
            filterChain.doFilter(request, response);
            return;
        }

        //不开启form，因为其不走该filter，采用basic

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null) {
            log.info("Basic Authentication Authorization header not found");
            Result r = Oauth2ExceptionCodeEnum.INVALID_REQUEST.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        header = header.trim();
        if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
            log.info("Basic Authentication Authorization header not found");
            Result r = Oauth2ExceptionCodeEnum.INVALID_REQUEST.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
            log.info("Empty basic authentication token");
            Result r = Oauth2ExceptionCodeEnum.UNAUTHORIZED_CLIENT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException e) {
            log.info("Failed to decode basic authentication token");
            Result r = Oauth2ExceptionCodeEnum.UNAUTHORIZED_CLIENT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        String token = new String(decoded, StandardCharsets.UTF_8);

        int delimit = token.indexOf(":");

        if (delimit == -1) {
            log.info("Invalid basic authentication token");
            Result r = Oauth2ExceptionCodeEnum.UNAUTHORIZED_CLIENT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        String clientId = token.substring(0, delimit);
        String clientSecret = token.substring(delimit + 1);

        if (StrUtil.isBlank(clientId) || StrUtil.isBlank(clientSecret)) {
            log.info("client_id or client_secret not found in form or basic");
            Result r = Oauth2ExceptionCodeEnum.INVALID_CLIENT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        ClientDetails clientDetails;
        try {
            clientDetails = this.clientDetailsService.loadClientByClientId(clientId);
        } catch (ClientRegistrationException e) {
            log.info("clientId [{}] not exist ", clientId);
            Result r = Oauth2ExceptionCodeEnum.INVALID_CLIENT.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        String bcryptSecret = clientDetails.getClientSecret();
        if (!passwordEncoder.matches(clientSecret, bcryptSecret)) {
            log.info("invalid clientSecret [}{]", clientSecret);
            Result r = Oauth2ExceptionCodeEnum.INVALID_CLIENT_SECRET.toResult();
            ResponseUtil.jsonResp(response).getWriter().write(JSONUtil.toJsonStr(r));
            return;
        }

        Collection<GrantedAuthority> authorities = clientDetails.getAuthorities();

        UsernamePasswordAuthenticationToken resultToken = new UsernamePasswordAuthenticationToken(clientId
                , bcryptSecret, authorities);

        SecurityContextHolder.getContext().setAuthentication(resultToken);

        filterChain.doFilter(request, response);
    }
}
